package com.culture.config.util;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

import com.culture.config.common.util.ThreadRunnable;

import org.apache.commons.collections.CollectionUtils;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class HashedWheelTimer extends ThreadRunnable{ 
    private final long tickDuration;
    private final Map<String,TimeoutTask> timeoutTaskMap;
    private final List<TimeoutTaskBucket> wheel;
    private final int mask;      
    private boolean isClosed = false;

    public HashedWheelTimer(long tickDuration, TimeUnit unit, int ticksPerWheel){ 
        super(1,true); 
        this.tickDuration = unit.toNanos(tickDuration);         
        ticksPerWheel = normalizeTicksPerWheel(ticksPerWheel);
        this.timeoutTaskMap = new HashMap<>(ticksPerWheel);
        this.wheel = new ArrayList<>(ticksPerWheel);         
        for (int i = 0; i < ticksPerWheel; i ++) {
            this.wheel.add(new TimeoutTaskBucket(i));
        }
        this.mask = wheel.size() - 1;  
        log.info("HashedWheelTimer initiated...");         
    }

    /**
     * start Timer
     */
    public boolean startTimer(){
        if(this.isClosed){
            return false;
        }
        if(!this.getRunFlag()){            
            return this.start(0);
        }
        return false;
    }

     /**
     *stop Timer;
     */
    public void stopTimer(){
        this.stop();
    }

    /**
     *close Timer and do not continue to use.
     */
    public void closeTimer(){
        this.isClosed = true; 
        this.wheel.forEach(LinkedList::clear);  
        this.shutdown();             
    }

    private int normalizeTicksPerWheel(int ticksPerWheel) {
        int normalizedTicksPerWheel = 1;
        while (normalizedTicksPerWheel < ticksPerWheel) {
            normalizedTicksPerWheel <<= 1;
        }
        return normalizedTicksPerWheel;
    }   

    @Override
    protected boolean runTask(final long startTime,final long ticks){
        if(this.isClosed){
            return false;
        }
        if(CollectionUtils.isEmpty(this.wheel)){
            return false;
        }       
        //run task after wait TickDuration
        final long deadline = waitForNextTick(startTime,ticks);
        if (deadline > 0) {
            int idx = (int) (ticks & mask);
            TimeoutTaskBucket timeOutTasks = this.wheel.get(idx);
            this.transferTimeoutsToWhell(startTime,ticks);
            this.handleTimeoutTasks(timeOutTasks,deadline);
        }
        //keep to true for HashedWheel
        return true;
    }
 
    /**
     * calculate goal nanoTime from startTime and current tick number,
     * then wait until that goal has been reached.
     * @return Long.MIN_VALUE if received a shutdown request,
     * current time otherwise (with Long.MIN_VALUE changed by +1)
     */
    private long waitForNextTick(final long startTime, final long ticks) {
        long deadline = tickDuration * (ticks + 1);
        for (;;) {
            final long currentTime = System.nanoTime() - startTime;
            long sleepTimeMs = (deadline - currentTime + 999999) / 1000000;

            if (sleepTimeMs <= 0) {
                if (currentTime == Long.MIN_VALUE) {
                    return -Long.MAX_VALUE;
                } else {
                    return currentTime;
                }
            }

            if (this.isWindows()) {
                sleepTimeMs = sleepTimeMs / 10 * 10;
            }

            try {
                Thread.sleep(sleepTimeMs);
            } catch (InterruptedException ignored) {
                return Long.MIN_VALUE;
            }
        }
    }

    private void transferTimeoutsToWhell(final long startTime,final long ticks) {
        List<TimeoutTask> timeoutTaskList = this.getNewTimeoutTasks();
        if(CollectionUtils.isEmpty(timeoutTaskList)){
            return;
        }
        timeoutTaskList.forEach(timeoutTask->{
            //check exist task
            if(this.timeoutTaskMap.containsKey(timeoutTask.getTaskKey())){
                return;
            }                  
            long delay = timeoutTask.delayWhitMillisecond();     
            long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(delay) - startTime;
            // Guard against overflow.
            if (delay > 0 && deadline < 0) {
                deadline = Long.MAX_VALUE;
            }
            // Add new timeout task       
            timeoutTask.deadline = deadline;       
            final long calculated = timeoutTask.deadline / this.tickDuration;
            timeoutTask.remainingRounds = (calculated - ticks) / this.wheel.size();           
            final long tick = Math.max(calculated, ticks); // Ensure we don't schedule for past.
            int stopIndex = (int) (tick & mask);
            TimeoutTaskBucket timeOutTasks = this.wheel.get(stopIndex);
            timeOutTasks.add(timeoutTask.get(timeOutTasks));
            this.timeoutTaskMap.put(timeoutTask.getTaskKey(), timeoutTask);
        });
    }   
   
    private void handleTimeoutTasks(TimeoutTaskBucket timeOutTasks, long deadline){       
        Iterator<TimeoutTask> iterator = timeOutTasks.iterator();
        while(iterator.hasNext()){
            TimeoutTask timeoutTask = iterator.next();
            if (timeoutTask.remainingRounds <= 0) {
                iterator.remove();
                this.timeoutTaskMap.remove(timeoutTask.getTaskKey());
                if (timeoutTask.deadline <= deadline) {
                    this.execTimeoutTask(timeoutTask.getTaskInfo());
                } else {
                    // The timeout was placed into a wrong slot. This should never happen.
                    throw new IllegalStateException(String.format(
                            "timeout.deadline (%d) > deadline (%d)", timeoutTask.deadline, deadline));
                }
            } else if (timeoutTask.isCancelled()) {
                iterator.remove();
                this.timeoutTaskMap.remove(timeoutTask.getTaskKey());
            } else {
                timeoutTask.remainingRounds --;
            }
        }
    }  

    protected boolean isWindows(){
        return System.getProperties().getProperty("os.name").toUpperCase().indexOf("WINDOWS") != -1;
    }    

    protected List<TimeoutTask> getNewTimeoutTasks(){
        return Collections.emptyList();
    }         

    protected void execTimeoutTask(Object taskInfo){
      // TODO document why this method is empty
    }

    //remove cancelled Task
    protected TimeoutTask removeCancelledTask(String taskKey){        
        TimeoutTask timeoutTask = this.timeoutTaskMap.get(taskKey);
        if(Objects.nonNull(timeoutTask)){
            Optional.ofNullable(timeoutTask.get()).ifPresent(e->e.remove(timeoutTask));
            this.timeoutTaskMap.remove(taskKey);
            timeoutTask.reset();
        }
        return timeoutTask;
    }

    /**
     * TimeoutTaskBucket
     */
    @Data
    public static class TimeoutTaskBucket extends LinkedList<TimeoutTask>{
        private final int wheelIndex;

        public TimeoutTaskBucket(int index){
            this.wheelIndex = index;
        }
    }

    /**
     * TimeoutTask
     */  
    public abstract static class TimeoutTask{   
        //wheel bucket
        private TimeoutTaskBucket bucket; 
        //deadline
        private long deadline;
        // remainingRounds will be calculated and set by Worker.transferTimeoutsToBuckets() before the
        // HashedWheelTimeout will be added to the correct HashedWheelBucket.
        private long remainingRounds;       
        // reset
        private void reset(){
            this.bucket = null;
            this.deadline = 0;
            this.remainingRounds = 0;
        }

        /**
         * get and save wheel ref
         * @param bucket
         * @return
         */
        private TimeoutTask get(TimeoutTaskBucket bucket){
            this.bucket = bucket;
            return this;
        }

        /**
         * get wheel ref
         * @return
         */
        private final TimeoutTaskBucket get(){
           return this.bucket;
        }

        /**
         * getTaskInfo
         * @return
         */
        protected abstract Object getTaskInfo();      
    
        /**
         * Returns {@code true} if and only if the {@link TimerTask} associated
         * with this handle has been cancelled.
         */
        protected abstract boolean isCancelled();  
       
        /**
        * next trigger wait time(millisecond)
        */
        protected abstract Long delayWhitMillisecond();

        /**
        * get task key
        */
        protected abstract String getTaskKey();
    }

}
